using System;
using System.Linq;
using System.Reflection;
using Nancy;
using Nancy.Bootstrapper;
using Nancy.Cookies;
using Nancy.Extensions;
using Nancy.Responses;
using QQ2564874169.Core;
using QQ2564874169.Core.Utils;
using QQ2564874169.WebFx.Nancy.Web;

namespace QQ2564874169.WebFx.Nancy.Middleware
{
    public class ClientKeeper
    {
        public ClientKeepOption Option { get; }
        private const char SplitChar = '_';
        private const string MustUpdateKey = "ClientKeepIdMustBeUpdate";

        static ClientKeeper()
        {
            NancyController.Created += controller =>
            {
                controller.ActionAfter += ActionsAfter;
            };
        }

        private static void ActionsAfter(object sender, ActionAfterEventArgs args)
        {
            var attr = args.Action.GetCustomAttribute<MustModifyKeepIdAttribute>();
            if (attr == null)
            {
                if (args.Action.DeclaringType != null)
                    attr = args.Action.DeclaringType.GetCustomAttribute<MustModifyKeepIdAttribute>();
                if (attr == null)
                    return;
            }
            var ctrl = (NancyController)sender;
            ctrl.Context.Items.Add(MustUpdateKey, attr);
            ctrl.ActionAfter -= ActionsAfter;
        }

        public ClientKeeper(IPipelines pipelines, ClientKeepOption options = null)
        {
            Option = options ?? new ClientKeepOption();
            pipelines.BeforeRequest.AddItemToStartOfPipeline(ReuqestBefore);
            pipelines.AfterRequest.AddItemToEndOfPipeline(RequestAfter);
        }

        private void RequestAfter(NancyContext context)
        {
            var user = context.CurrentUser as ClientUser;
            if (user == null)
            {
                WriteKeepId(context, null);
                return;
            }
            if (user.IsLogin == false && user.IsLogout == false)
            {
                if (user.Param.IsAbsTime)
                {
                    return;
                }
                if (!context.Items.ContainsKey(MustUpdateKey) && !KeepIdChanging(context))
                {
                    return;
                }
            }
            WriteKeepId(context, user);
        }

        protected virtual string CreateKeepId(ClientParam param)
        {
            var ltime = ConvertHelper.ToLongTime(param.Expire);
            var isabs = param.IsAbsTime ? 1 : 0;
            var sign = param.GetSign(Option.Secret);
            return string.Join("_", param.UserId, isabs, ltime, sign);
        }

        protected virtual void WriteKeepId(NancyContext context, ClientUser user)
        {
            if (user == null && context.Request.Cookies.ContainsKey(Option.KeepName))
            {
                WriteToResponse(context.Response, "", DateTime.Now.AddDays(-1));
            }
            else if (user != null && user.IsLogout)
            {
                WriteToResponse(context.Response, "", DateTime.Now.AddDays(-1));
            }
            else if (user != null)
            {
                user.Param.Expire = UpdateExpires(user.Param);
                var kid = CreateKeepId(user.Param);
                WriteToResponse(context.Response, kid, user.Param.Expire);
            }
        }

        private Response ReuqestBefore(NancyContext context)
        {
            var kid = ReadKeepId(context.Request);
            if (string.IsNullOrWhiteSpace(kid))
                return null;
            var param = ParseKeepId(kid);
            if (param == null)
                return null;
            if (param.Expire < DateTime.Now.ToUniversalTime())
                return null;
            var user = Option.GetClientUser(param);
            if (user == null)
                return null;
            user.Param = param;
            user.IsLogin = true;
            user.IsLogout = false;
            context.CurrentUser = user;
            return null;
        }

        protected virtual string ReadKeepId(Request request)
        {
            if (request.Cookies.ContainsKey(Option.KeepName))
                return request.Cookies[Option.KeepName];
            var values = request.Headers[Option.KeepName]?.ToArray();
            if (values != null && values.Any())
                return values.FirstOrDefault();
            return null;
        }

        protected virtual ClientParam ParseKeepId(string keepId)
        {
            if (string.IsNullOrEmpty(keepId))
                return null;
            var items = keepId.Split(SplitChar);
            if (items.Length != 4)
                return null;
            var uid = items[0];
            var isabs = items[1];
            var stime = items[2];
            var sign = items[3];
            long ltime;
            if (long.TryParse(stime, out ltime) == false)
                return null;
            var model = new ClientParam
            {
                Expire = ConvertHelper.ToDate(ltime),
                IsAbsTime = isabs == "1",
                UserId = uid,
                IsLogin = false
            };
            var signstr = model.GetSign(Option.Secret);
            if (signstr.QQEquals(sign) == false)
                return null;
            return model;
        }

        protected virtual DateTime UpdateExpires(ClientParam param)
        {
            if (param.IsAbsTime)
            {
                return param.Expire;
            }
            return DateTime.Now.AddMinutes(Option.LogoutWhenIdle);
        }

        protected virtual bool KeepIdChanging(NancyContext context)
        {
            if (!"get".QQEquals(context.Request.Method))
                return false;
            if (context.Request.IsAjaxRequest() && !Option.KeepIdUpdateWhenAjax)
                return false;
            if (!(context.Response is HtmlResponse))
                return false;
            if (context.Response is MaterialisingResponse)
            {
                var mres = (MaterialisingResponse)context.Response;
                var fs = mres.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
                foreach (var item in fs)
                {
                    if (item.FieldType.IsAssignableFrom(typeof(Response)))
                    {
                        var res = item.GetValue(context.Response);
                        if (!(res is HtmlResponse))
                            return false;
                        break;
                    }
                }
            }
            return true;
        }

        protected void WriteToResponse(Response response, string keepId, DateTime expire)
        {
            if (response == null)
                return;

            var cookies = response.Cookies;
            if (cookies != null && cookies.Count > 0)
            {
                for (var i = 0; i < cookies.Count; i++)
                {
                    if (Option.KeepName.QQEquals(cookies[i].Name))
                    {
                        cookies.RemoveAt(i--);
                    }
                }
            }
            if (Option.SaveToCookie)
                response.WithCookie(new NancyCookie(Option.KeepName, keepId, expire));
            if (Option.SaveToHeader)
                response.WithHeader(Option.KeepName, keepId);
        }

        public ClientUser Keep(NancyContext context, string userId, object state = null,
            DateTime? absoluteExpiration = null)
        {
            var param = new ClientParam
            {
                IsAbsTime = absoluteExpiration.HasValue,
                Expire = absoluteExpiration ?? DateTime.Now,
                UserId = userId,
                IsLogin = true,
                State = state
            };
            var user = Option.GetClientUser(param);
            user.Param = param;
            user.IsLogin = true;
            user.IsLogout = false;
            context.CurrentUser = user;
            return user;
        }

        public void CancelKeep(NancyContext context)
        {
            var user = context.CurrentUser as ClientUser;
            if (user == null) return;
            user.IsLogin = false;
            user.IsLogout = true;
        }
    }
}
